% !Mode:: "TeX:UTF-8"
\chapter{异常处理}
\label{chap:java_exception}

程序在运行过程中，出现异常了怎么办？是崩溃退出还是给用户一个友善的提示。
通过本章的学习，你将了解什么是异常、如何处理异常，又如何自定义异常。
异常可分为以下三类：系统错误、异常和运行时异常。

\begin{itemize}
\item[1.]系统错误(Error):\newline
是由Java虚拟机抛出的，代表的是一些比较严重的错误，这样的错误很少发生，一旦发生无法挽回。
\item[2.] 异常(Exception):\newline
是由程序或外部环境引起的，如打开不存在的文件等。
\item[3.] 运行时异常(RuntimeException):\newline
主要指的程序中出现的错误，如数组下标越界、类型转换错误等。
\end{itemize}

其中，系统错误(Error)会导致程序的退出或者崩溃，通常无法挽回；
而运行时异常(RuntimeException)是程序实现过程中引入的错误，可通过修改程序解决，
所以这两类异常（包括其子类）称为{\kaishu 免检异常}(Unchecked Exception)，不要求在代码中进行额外处理。
而其他异常称为{\kaishu 需检异常}(Checked Exception)，必须编写处理代码。
如何进行检查并进行处理呢？

文件不存在、网络已断开等异常，都是常见的可预见的异常，对于这类异常应加以保护。
传统的编程语言，通常先检测文件是否存在，再去执行读写操作。但这种异常检测行为，可能会被很多人忽略。
而Java语言中，常见异常（Exception）基本都是\emph{checked异常}，不允许无视。

\begin{lstlisting}[language=Java]
  File file = new File("test.txt");
  if (file.exists()) {
      readFile(file);
  } else {
      System.out.println("文件不存在！");
  }
\end{lstlisting}

使用异常可强制所有人，遵守相同的规则而提高代码质量。它也是一种\emph{WARN声明}，使用该API存在哪些风险，你要做好保护。
\begin{lstlisting}[language=Java]
  File file = new File("test.txt");
  try {
      readFile(file);
  } catch (IOException e) {
      e.printStackTrace();
  }
\end{lstlisting}

以上代码中，readFile显式抛出（throws）读写异常，必须使用\lstinline{try...catch...}的形式。
在\emph{try}块中，可以忽视任何异常，不需要考虑意外情况；而在\emph{catch}中捕获对应的异常，并加以处理。
组成异常语句的关键字有：\lstinline{try}、\lstinline{catch}、\lstinline{finally}。
其中，try可以带有参数，而catch必须明确异常类型的参数，finally用于最终收尾只是一个语句块。
JDK提供了一些基本的异常类型，如\figref{fig:part1_exceptionclasses}所示。

\begin{figure}[!htb]
\centering
\small
\begin{tikzpicture}
\umlsimpleclass[x=0, y=0, anchor=east]{Throwable}
\umlsimpleclass[x=3, y=1, anchor=east]{Error}
\umlsimpleclass[x=3, y=-1, anchor=east]{Exception}
\umlsimpleclass[x=9, y=2, anchor=east]{VirtualMachineError}
\umlsimpleclass[x=8, y=1, anchor=east]{AssertionError}

\umlsimpleclass[x=5, y=0, anchor=west]{IOException}
\umlsimpleclass[x=4, y=-2.5, anchor=east]{RunTimeException}

\umlsimpleclass[x=5, y=-1, anchor=west]{ArithmeticException}
\umlsimpleclass[x=5, y=-2, anchor=west]{IndexOutOfBoundsException}
\umlsimpleclass[x=5, y=-3, anchor=west]{IllegalArgumentException}

\umlHVHinherit[arm2=1.6]{Error}{Throwable}
\umlHVHinherit[arm2=1.6]{Exception}{Throwable}

\umlHVHinherit[arm2=1.6]{VirtualMachineError}{Error}
\umlHVHinherit[arm2=1.6]{AssertionError}{Error}

\umlHVinherit{IOException}{Exception}
\umlHVinherit{RunTimeException}{Exception}

\umlHVHinherit[arm2=2.6]{ArithmeticException}{RunTimeException}
\umlHVHinherit[arm2=2.6]{IndexOutOfBoundsException}{RunTimeException}
\umlHVHinherit[arm2=2.6]{IllegalArgumentException}{RunTimeException}
\end{tikzpicture}
\caption{UML：异常类结构图}
\label{fig:part1_exceptionclasses}
\end{figure}

异常类的实现代码都非常简单，主要是起到分类和提示的作用，想自定义一个异常也非常简单。
先看一个\emph{IOException}的代码。

\begin{lstlisting}[language=Java]
public class IOException extends Exception {
    static final long serialVersionUID = 7818375828146090155L;

    public IOException() {
        super();
    }

    public IOException(String message) {
        super(message);
    }

    public IOException(String message, Throwable cause) {
        super(message, cause);
    }

    public IOException(Throwable cause) {
        super(cause);
    }
}
\end{lstlisting}


\section{异常语句}
鼓励使用异常，但不建议用异常做业务逻辑，有异常处理的代码无论是生成的class文件还是运行时都比较占用资源。
上一节说过，有3个关键字和异常语句有关。其中，try和finally只能有一个，而catch语句优先匹配第一个合适的异常类型。
再强调一次面向对象的概念：继承关系是一种is a关系。

\begin{lstlisting}[language=Java]
  try(FileInputStream fis = new FileInputStream(file)) {
      fis.read();
  } catch (FileNotFoundException e) {
      System.out.println("文件不存在");
  } catch (IOException e) {
      System.out.println("读文件时遇到错误");
  } finally {
      System.out.println("无论如何，都到此善尾");
  }
\end{lstlisting}

以上代码，\emph{FileNotFoundException}是一个\emph{IOException}。
如果交换它们之间的顺序，\emph{FileNotFoundException}就是一条无效语句，编译也无法通过。
try从JDK7之后才可以包含参数，准确的说是一段代码。
其中定义的变量必须是实现了\emph{AutoCloseable}的对象，因此可省掉关闭资源的步骤。

\begin{lstlisting}[language=Java]
class MyAutoCb implements AutoCloseable {
    public void print() {
        System.out.println("我要输出我自己");
    }
    @Override
    public void close() throws Exception {
        System.out.println("我被关了....");
    }
}

try(MyAutoCb cb = new MyAutoCb();FileInputStream fis = new FileInputStream(file)) {
    fis.read();
    cb.print();
}省略...
\end{lstlisting}

\noindent
以上代码中，\emph{MyAutoCb}对象的close函数会自动被调用。接着说，异常语句的组合方式。
catch和finally是不能单独存在的，而try虽然可以但没有什么意义，其中的异常又被再次抛出。
常见的书写方式：

\begin{lstlisting}[language=Java]
 try {
 } catch(XxxException e) { // 捕获到的异常，还可以重写抛出
   throw e; // 再次抛出
 }

 try {
 } catch(XxxException e) {
 } finally { // 先try语句，再catch语句，最后finall语句，无异常也会执行finally
 }

 try {
 } finally { // 有没有异常不关心，但出现异常之后我有机会先执行
 }

 try {
 } catch(XxxException e) { // 多个异常类型，可以合并为一个catch语句
 } catch(XxxException e) { // 存在继承关系的情况，子类放在前面，并且不能使用|合并
 }
\end{lstlisting}

应避免在finally语句中再次抛出异常，它会覆盖原有异常，也会覆盖try语句中的return语句。

\section{自定义异常}
\emph{异常}都是被\lstinline{throw}语句抛出来的，且只有Throwable类型的对象才可以被抛出。
因此，继承某个已知异常或者\emph{Throwable}就可以实现自定义。
再重申一点：运行时异常还是\emph{Checked异常}？
\begin{lstlisting}[language=Java,mathescape]
void throwRuntime() {
    throw new MyRunException("运行时遇到异常");
}

// 没有正确抛出异常，提示错误
void throwChecked() {
    $\color{red}\uwave{
    throw\hspace{2mm}new\hspace{2mm}MyCheckedException(\text{"}\text{我要在编译时检查}\text{"});
    }$
}

void throwError() {
    throw new MyError("运行时遇到错误");
}
\end{lstlisting}
\noindent
上述代码，\emph{throwChecked}没显式声明要抛出的异常编译失败，正确的做法如下：
\begin{lstlisting}[language=Java]
void throwChecked() throws MyCheckedException {
    throw new MyCheckedException("我会在编译时检查");
}
\end{lstlisting}
\noindent
使用它的代码，必须使用try...catch...语句或者重新throw才能编译通过，也就是以下2种方式：
\begin{lstlisting}[language=Java]
// 再次抛出异常
void testException1() throws MyCheckedException {
    throwChecked();
}
// 捕获异常并处理掉，或者再次throw e
void testException2() {
    try {
        throwChecked();
    } catch (MyCheckedException e) {
        e.printStackTrace();
    }
}
\end{lstlisting}

\section{本章总结}
通过本章的学习，可知异常分为\emph{Checked}和\emph{UnChecked}2种。
其中，\emph{Checked异常}需要显式处理，而\emph{RunTimeException}和\emph{Error}都不要求捕获。
无论何种类型的异常，都是\emph{Throwable}的子类，可以被抛出\emph{throw}和捕获\emph{catch}。
通常不建议捕获Erro，像内存不足、虚拟机错误之类的错误也许根本捕获不到。
即使捕获到Error也不能阻止程序（线程）的崩溃，若只是多线程的一个线程崩溃了并不是大问题。
你还可以做拯救你的软件，即使线程\emph{栈溢出}了，也还可以重新再启动一个。

